package com.ccteam.fluidmusic.fluidmusic.media


import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
import android.os.*
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.media.MediaBrowserServiceCompat
import com.ccteam.fluidmusic.fluidmusic.R
import com.ccteam.fluidmusic.fluidmusic.common.METADATA_NOTHING_PLAYING
import com.ccteam.fluidmusic.fluidmusic.common.PlaylistManager
import com.ccteam.fluidmusic.fluidmusic.common.utils.*
import com.ccteam.fluidmusic.fluidmusic.common.utils.notification.NotificationHandler
import com.ccteam.fluidmusic.fluidmusic.media.extensions.*
import com.ccteam.fluidmusic.fluidmusic.media.library.BrowseTree
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
import com.google.android.exoplayer2.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.database.ExoDatabaseProvider
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.cache.*
import com.google.android.exoplayer2.util.Util
import com.orhanobut.logger.Logger
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import java.io.File
import javax.inject.Inject

/**
 * @author Xiaoc
 * @since 2020.12.25
 *
 * FluidMusic的服务类
 * 此服务负责音乐内容相关的检索和播放
 * 实现 [MediaBrowserServiceCompat] 用于音乐内容检索，并控制
 * 由于 [MediaBrowserServiceCompat.onLoadChildren] 的限制，仅提供本地音乐检索服务，在线音乐自己实现
 *
 * 播放音乐基于 [com.google.android.exoplayer2.ExoPlayer] 组件
 */
@AndroidEntryPoint
class FluidMusicService : MediaBrowserServiceCompat() {

    private val serviceJob = SupervisorJob()
    private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)

    private var isForegroundService = false

    private lateinit var notificationHandler: NotificationHandler
    private lateinit var exoPlayer: ExoPlayer
    private lateinit var mediaSession: MediaSessionCompat
    private lateinit var mediaSessionConnector: MediaSessionConnector

    private lateinit var fluidMusicPlaybackPreparer: FluidMusicPlaybackPreparer
    private lateinit var fluidMusicQueueEditor: FluidMusicQueueEditor

    // 初始化BrowseTree媒体浏览树
    @Inject lateinit var browserTree: BrowseTree

    private lateinit var mediaSource: ConcatenatingMediaSource

    private val playerEventListener = PlayerEventListener()

    @Inject lateinit var playlistStorage: PlaylistStorage

    @Inject lateinit var albumArtCache: AlbumArtCache

    private lateinit var trackSelector: DefaultTrackSelector

    @ExperimentalCoroutinesApi
    override fun onCreate() {
        super.onCreate()

        // 设置PendingIntent，用于通知栏点击启动Activity，使用隐式Intent启动
        val sessionActivityPendingIntent:PendingIntent = PendingIntent.getActivity(this,0,Intent(START_ACTION),0)

        // 初始化MediaLibrarySession会话
        mediaSession = MediaSessionCompat(this, SESSION_TAG).apply {
            isActive = true
            setSessionActivity(sessionActivityPendingIntent)

            //设置token后会触发MediaBrowserCompat.ConnectionCallback的回调方法
            //表示MediaBrowser与MediaBrowserService连接成功
            setSessionToken(sessionToken)
        }

        // 初始化通知栏处理器
        notificationHandler = NotificationHandler(
            albumArtCache,
            this,
            mediaSession,
            PlayerNotificationListener()
        )

        // 基本数据初始化完毕后初始化MediaSessionConnector、ExoPlayer等内容
        initMediaSessionConnector()
    }

    /**
     * 初始化播放器和MediaSessionConnector
     */
    private fun initMediaSessionConnector(){
        val audioAttributes = AudioAttributes.Builder().apply {
            setContentType(C.CONTENT_TYPE_MUSIC)
            setUsage(C.USAGE_MEDIA)
        }.build()

        trackSelector = DefaultTrackSelector(this)

        val renderFactory = DefaultRenderersFactory(this)
            .setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER)

        // 初始化默认ExoPlayer的数据源工厂，基类使用HttpDataSource
        val (dataSourceFactory,defaultFactory) = initDataSourceFactory()

        // 初始化ExoPlayer
        exoPlayer = SimpleExoPlayer.Builder(this,
            renderFactory).apply {
            setAudioAttributes(audioAttributes,true)
            setHandleAudioBecomingNoisy(true)
            setTrackSelector(trackSelector)
        }.build().apply {
            addListener(playerEventListener)
        }


        /**
         * 使用ConcatenatingMediaSource，该MediaSource支持使用多个MediaSource填充在播放列表中
         * 并使用自己的ShuffleOrder策略
         */
        mediaSource = ConcatenatingMediaSource(false, true,FluidMusicShuffleOrder(0))
        exoPlayer.setMediaSource(mediaSource)
        exoPlayer.prepare()

        fluidMusicPlaybackPreparer = FluidMusicPlaybackPreparer(defaultFactory,playlistStorage,browserTree,exoPlayer,dataSourceFactory,mediaSource)
        fluidMusicQueueEditor = FluidMusicQueueEditor(defaultFactory,browserTree,mediaSource,dataSourceFactory,playlistStorage)

        // 初始化MediaSessionConnector，用于支持MediaSession各种事件回调自定义
        mediaSessionConnector = MediaSessionConnector(mediaSession).apply {
            setPlayer(exoPlayer)
            setPlaybackPreparer(fluidMusicPlaybackPreparer)
            setMediaMetadataProvider(ExtraMediaMetadataProvider())
            setQueueNavigator(QueueNavigator(mediaSession))
            setControlDispatcher(FluidMusicControlDispatcher())
            setQueueEditor(fluidMusicQueueEditor)
        }
        mediaSessionConnector.registerCustomCommandReceiver(BitrateTrackCommandReceiver(trackSelector))

        // 准备已存在的播放列表
        mediaSession.controller.transportControls.prepare()
    }

    /**
     * 初始化数据源工厂
     * 主要以下步骤
     * 1.获取缓存Cache，如果存在则使用缓存数据源工厂，如果不存在，使用默认工厂
     * 2.设置缓存策略，允许写入并忽略缓存错误
     *
     * 会返回两种类型的Factory工厂
     * 前者是带缓存的Factory，用于当播放在线音乐时使用
     * 后者是不带缓存的Factory，用于本地音乐播放时使用
     */
    private fun initDataSourceFactory(): Pair<DataSource.Factory,DataSource.Factory>{
        val upstreamFactory = DefaultDataSourceFactory(applicationContext,Util.getUserAgent(/* context= */ this, "fluidmusic"))
        val cache = initCache()

        // 设置缓存的相关内容
        return CacheDataSource.Factory()
            .setCache(cache)
            .setUpstreamDataSourceFactory(upstreamFactory)
            .setCacheWriteDataSinkFactory(CacheDataSink.Factory().setCache(cache))
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) to upstreamFactory
    }

    /**
     * 创建缓存策略
     * 获取要缓存数据的目录以及缓存数据的大小
     * 这里设置缓存大小为400M
     */
    private fun initCache(): Cache{
        val directory = applicationContext.externalCacheDirs
        val file = if(directory.isNullOrEmpty()){
            applicationContext.filesDir
        } else {
            directory[0]
        }
        val cacheDirectory = File(file,"media")
        val databaseProvider = ExoDatabaseProvider(applicationContext)
        return SimpleCache(cacheDirectory,
            LeastRecentlyUsedCacheEvictor(400 * 1024 * 1024),databaseProvider)
    }

    /**
     * 当用户使用后台管理任务将APP终止时调用此方法
     * 此时该做的就是存储当前播放的内容
     * 然后停止音乐播放
     * 当stop后会调用通知栏[PlayerNotificationManager.NotificationListener.onNotificationCancelled]
     * 停止掉当前服务内容
     */
    override fun onTaskRemoved(rootIntent: Intent?) {
        saveCurrentMusicToStorage()

        exoPlayer.stop()
        exoPlayer.clearMediaItems()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 服务销毁后关闭MediaSession
        mediaSession.run {
            release()
        }

        // 取消服务协程
        serviceJob.cancel()

        // 结束ExoPlayer
        exoPlayer.removeListener(playerEventListener)
        exoPlayer.release()

        // 结束通知栏内容
        notificationHandler.close()
        // 结束准备器中的协程任务
        fluidMusicPlaybackPreparer.close()
        fluidMusicQueueEditor.close()
    }


    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot {
        return BrowserRoot(MediaIDHelper.LOCAL_MEDIA_ROOT,null)
    }

    override fun onLoadChildren(
        parentId: String,
        result: Result<List<MediaBrowserCompat.MediaItem>>
    ) {
        onLoadChildren(parentId,result, Bundle())
    }


    /**
     * [MediaBrowserCompat] 框架提供的加载子目录方法
     * 由于该方法的限制，所以在线音乐内容检索不依靠此方法加载
     * 仅适配本地音乐加载
     */
    override fun onLoadChildren(
        parentId: String,
        result: Result<List<MediaBrowserCompat.MediaItem>>,
        options: Bundle
    ) {
        result.detach()

        // 如果Bundle存在重新扫描命令，则执行重新扫描
        val isRescan = !options.getString(MediaIDHelper.OPTION_LOCAL_MEDIA_RESCAN,null).isNullOrEmpty()
        serviceScope.launch {

            // 从媒体树种获得本地音乐数据
            browserTree.getChildren(parentId,object :BrowseTree.LoadCallback{

                override fun onSuccess(metadataList: List<MediaMetadataCompat>?) {
                    val children = metadataList?.map { item ->
                        MediaBrowserCompat.MediaItem(item.toMediaDescription(), item.flag)
                    } ?: listOf()
                    result.sendResult(children)
                }

                override fun onFail(errorMsg: String) {
                    val bundle = bundleOf("errorMsg" to errorMsg)
                    result.sendError(bundle)
                }

            },isRescan)
        }

    }

    /**
     * 通知栏调用回调
     */
    private inner class PlayerNotificationListener: PlayerNotificationManager.NotificationListener{
        /**
         * 当通知栏更新后调用
         * 在此可以设置是否将其置于前台服务
         * 前台服务需要对应前台服务权限
         *
         * @param ongoing 通知是否进行中
         */
        override fun onNotificationPosted(
            notificationId: Int,
            notification: Notification,
            ongoing: Boolean
        ) {
            if(ongoing && !isForegroundService){
                ContextCompat.startForegroundService(
                    applicationContext,
                    Intent(applicationContext,this@FluidMusicService.javaClass)
                )
                // 开启前台服务
                startForeground(notificationId,notification)
                isForegroundService = true
            }
        }

        /**
         * 当通知栏被取消
         * 此时说明程序退出，应该取消通知栏显示
         */
        override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
            stopForeground(true)
            isForegroundService = false
            stopSelf()
        }
    }

    /**
     * 当前播放媒体的MediaMetadata提供者
     * 因在播放时设置了播放列表，可以直接使用播放列表存储的Metadata进行设置
     */
    private inner class ExtraMediaMetadataProvider: MediaSessionConnector.MediaMetadataProvider{

        override fun getMetadata(player: Player): MediaMetadataCompat {
            if (player.currentTimeline.isEmpty) {
                return METADATA_NOTHING_PLAYING
            }
            // 正在播放的歌曲Metadata需要加入处于播放列表的位置索引，方便准确知道在播放哪首播放列表的歌
            val currentIndex = player.currentWindowIndex
            return MediaMetadataCompat.Builder(PlaylistManager.getItem(currentIndex))
                .putLong(METADATA_KEY_QUEUE_ID,currentIndex.toLong()).build()
        }

    }

    /**
     * Queue队列抽屉管理者
     * 该管理者管理着最多10个Queue队列歌曲信息
     * 在播放歌曲时会进行附近10首歌曲的基本信息加载，用于更快显示离该歌曲最近的一些歌曲的基本信息
     * 这里返回的 [MediaDescriptionCompat] 也和通知栏显示的内容息息相关
     * 我们这里还传入了Extra字段，加入了自定义的一些数据填充进去
     */
    private inner class QueueNavigator(mediaSession: MediaSessionCompat): TimelineQueueNavigator(mediaSession){

        override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
            val mediaMetadata = PlaylistManager.getItem(windowIndex)
            return mediaMetadata.toMediaDescription()
        }

    }

    /**
     * 播放器事件监听回调
     */
    private inner class PlayerEventListener: Player.EventListener{

        /**
         * 当播放状态发生变化时调用
         * @param state 播放状态
         */
        override fun onPlaybackStateChanged(state: Int) {

            // 根据对应播放状态决定是否显示通知栏
            when(state){
                Player.STATE_BUFFERING,
                Player.STATE_READY ->{
                    notificationHandler.showNotification(exoPlayer)
                    // 存储当前播放的内容
                    if(state == Player.STATE_READY){
                        saveCurrentMusicToStorage()
                    }
                }
                else -> {
                    notificationHandler.hideNotification()
                }
            }
        }

        override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
            if(!playWhenReady){
                stopForeground(false)
            }
            // 当按下播放键时重新存储当前播放的内容
            if(reason == PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST && playWhenReady){
                saveCurrentMusicToStorage()
            }
        }

        override fun onRepeatModeChanged(repeatMode: Int) {
            saveCurrentMusicToStorage()
        }

        override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
            saveCurrentMusicToStorage()
        }

        /**
         * ExoPlayer错误处理
         */
        override fun onPlayerError(error: ExoPlaybackException) {
            val message = when(error.type){
                ExoPlaybackException.TYPE_SOURCE ->{
                    Logger.d("TYPE_SOURCE: " + error.sourceException)
                    R.string.error_media_not_found
                }
                ExoPlaybackException.TYPE_REMOTE ->{
                    Logger.d("TYPE_REMOTE: " + error.message)
                    R.string.error_media_not_found
                }
                else ->{
                    Logger.d("TYPE_RENDERER: " + error.rendererException)
                    R.string.generic_error
                }
            }
            Toast.makeText(
                applicationContext,
                message,
                Toast.LENGTH_LONG
            ).show()
        }
    }

    private fun saveCurrentMusicToStorage(){

        serviceScope.launch {
            playlistStorage.saveCurrentModel(
                exoPlayer.currentWindowIndex,
                exoPlayer.currentPosition,
                exoPlayer.repeatMode,
                exoPlayer.shuffleModeEnabled
            )
        }
    }

    companion object {
        const val SESSION_TAG = "FluidMusic"
        const val START_ACTION = "com.ccteam.fluidmusic.NOTIFICATION_START"
    }

}